Skip to content

Support --default(…) in --value(…) and --modifier(…) to support fallback values#19989

Merged
RobinMalfait merged 7 commits into
mainfrom
feat/support-default-css-functional-utility
May 4, 2026
Merged

Support --default(…) in --value(…) and --modifier(…) to support fallback values#19989
RobinMalfait merged 7 commits into
mainfrom
feat/support-default-css-functional-utility

Conversation

@RobinMalfait
Copy link
Copy Markdown
Member

@RobinMalfait RobinMalfait commented Apr 28, 2026

This PR adds a new --default(…) option that can be used inside --value(…) or --modifier(…) such that functional utilities without an explicit value/modifier can still be defined as a functional utility.

For example, let's say you have a tab-* utility:

@utility tab-* {
  tab-size: --value(number, --default(4));
}

This allows us to use tab-1, tab-2, tab-4 and so on. But it also allows us to use just tab. In this case the tab-size would use a default value of 4:

.tab {
  tab-size: 4;
}

Main motivation is to be able to re-implement utilities such as shadow/50 purely in CSS. It's also something we support in the JS based APIs, but not in the CSS based one, so while it's a "new" feature, it's more like a missing feature right now, and often a reason for people to use the JS based APIs instead.

For consistency reasons, this is also implemented for --modifier(…) such that you can use a default value there. E.g. when re-implementing text-sm where a default line-height is set without the explicit use of a modifier.

Other approaches I considered

Used the explicit --default(…) argument of --value(…) for a few reasons.

  1. It's explicit about being a fallback value. If you have @utility foo-*, then you want to be able to use foo, but foo-bad should not compile.
  2. When --value(…) is used in (complex) property values (think a bunch of calc(…) expressions), then we don't need a separate property for this.

One of the ideas was to have a literal fallback:

@utility tab-* {
  tab-size: 4;
  tab-size: --value(number);
}

For tab, this would compile to:

.tab {
  tab-size: 4;
}

For tab-123, this would compile to:

.tab {
  tab-size: 4;
  tab-size: 123;
}

Getting rid of the tab-size: 4 would be an option, but it's a common pattern in real CSS for fallback values (think hex background color, over a more modern oklch color).

For tab-foo, this would compile to:

.tab {
  tab-size: 4;
}

This syntax without the --default(…) also means repetition of certain properties. Add --modifier(…) to the mix, and there is even more repetition going on.

Another option to consider is that the default fallback is just another option in the --value(…, 4), but if a default fallback is a keyword, then there is a chance that this might conflict with actual keywords we interpret.

Fixes: #16824

Test plan

  1. Added a handful of new tests to make sure this functionality works
  2. Existing tests still pass

@RobinMalfait RobinMalfait marked this pull request as ready for review April 29, 2026 09:43
@RobinMalfait RobinMalfait requested a review from a team as a code owner April 29, 2026 09:43
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 29, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds tests in packages/tailwindcss/src/utilities.test.ts asserting compiled CSS is empty when `--value(...)` is omitted or unresolved, and that `--default(...)` nested inside `--value(...)` enables resolution (including inside `calc(...)` and alongside `--modifier(...)`). Parser normalization no longer appends a trailing `-*` when a functional `(` is present. Validation now treats a missing candidate `value` as eligible for `--default(...)` lookup and requires at least one resolvable `--value(...)` branch; modifier resolution remains separate. No public API changes.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR addresses issue #16824 by enabling modifier support for utilities without explicit arguments through the --default(…) mechanism, allowing utilities like shadow/50 to work with CSS-only declarations.
Out of Scope Changes check ✅ Passed All changes are focused on adding --default(…) support within --value(…) and related test coverage, with no unrelated modifications present.
Description check ✅ Passed The PR description clearly explains the new --default(…) feature, its purpose, design rationale, motivation, and test coverage.
Title check ✅ Passed The PR title accurately reflects the main feature: adding --default(…) support inside --value(…) and --modifier(…) to provide fallback values for functional utilities.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@RobinMalfait RobinMalfait force-pushed the feat/support-default-css-functional-utility branch 4 times, most recently from 754bfc8 to 9dbd228 Compare May 3, 2026 17:45
@RobinMalfait RobinMalfait changed the title Add --default(…) option in --value(…) to support fallback values Support --default(…) in --value(…) and --modifier(…) to support fallback values May 3, 2026
@RobinMalfait RobinMalfait force-pushed the feat/support-default-css-functional-utility branch from 9dbd228 to 24b152e Compare May 3, 2026 17:51
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 4, 2026

Confidence Score: 5/5

Safe to merge — no P0 or P1 issues found; implementation is correct and well-tested.

The logic changes are internally consistent: the nullable-value short-circuit in resolveValueFunction, the updated --modifier guard (modifier !== null), the --default(…) function recognition in the storage arg-normalization pass, and the theme-key auto-suffix guard (!arg.includes('(')) all work together correctly. Five targeted tests cover the key combinations. No behavioral regressions on existing paths were found.

No files require special attention.

Reviews (3): Last reviewed commit: "remove unnecessary check" | Re-trigger Greptile

Comment thread CHANGELOG.md
RobinMalfait added a commit that referenced this pull request May 4, 2026
…20005)

While working on #19989, I noticed that `--value(…)` inside functional
`@utility` definitions is not required right now.

That means that the following CSS is valid:
```css
@Utility foo-* {
  color: red;
}
```

But this doesn't really makes sense, because this now accepts a value
and `foo-a`, `foo-b` and `foo-c` would generate the following CSS:
```css
.foo-a {
  color: red;
}
.foo-b {
  color: red;
}
.foo-c {
  color: red;
}
```
The `a`, `b`, and `c` are not doing anything here apart from making your
CSS bigger. So this is very likely an actual bug that you forgot to use
`--value(…)`.

Additionally, if a `--value(…)` was used, but it didn't resolve
anything, then we already properly discared the candidate.

## Test plan

1. Add test to ensure `--value(…)` is required in functional `@utility`
definitions
2. Existing tests pass
This allows us to re-implement functional utilities with a default value
in CSS using `@utility`.

Used the explicit `--default()` argument of `--value()` for a few reasons.

1. It's explicit about being a falllback value. If you have `@utility
   foo-*`, then you want to be able to use `foo`, but `foo-bad` should
   not compile.
2. When `--value(…)` is used in (complex) property values (think a bunch
   of `calc(…)` expressions), then we don't need a separate property for
   this.

One of the ideas was to have a literal fallback:
```css
@Utility tab-* {
  tab-size: 4;
  tab-size: --value(number);
}
```

For `tab`, this would compile to:
```css
.tab {
  tab-size: 4;
}
```

For `tab-123`, this would compile to:
```css
.tab {
  tab-size: 4;
  tab-size: 123;
}
```
Getting rid of the `tab-size: 4` would be an option, but it's a common
pattern in real CSS for fallback values (think hex background color,
over a more modern `oklch` color).

For `tab-foo`, this would compile to:
```css
.tab {
  tab-size: 4;
}
```
Which means that we have an infinite amount classes that would result in
the same class, which is bad. We could special case this one because the
internal `value` would still be `null`, but it might be too confusing.

This syntax without the `--default` also means repetition of certain
properties.

Add `--modifier(…)` to the mix, and there is even more repetition going
on.

Another option to consider is that the default fallback is just another
option in the `--value(…, 4)`, but if a default fallback is a keyword,
then there is a chance that this might conflict with actual keywords we
interpret.
@RobinMalfait RobinMalfait force-pushed the feat/support-default-css-functional-utility branch from 24b152e to d196a77 Compare May 4, 2026 14:46
If we didn't resolve even if we have a modifier, then we _also_ should
remove the declaration and stop immediately. So both branches are the same.
@RobinMalfait RobinMalfait merged commit 08cad84 into main May 4, 2026
7 of 8 checks passed
@RobinMalfait RobinMalfait deleted the feat/support-default-css-functional-utility branch May 4, 2026 15:03
RobinMalfait added a commit to tailwindlabs/tailwindcss.com that referenced this pull request May 11, 2026
Direct link:
https://tailwindcss-com-git-feat-document-default-fn-tailwindlabs.vercel.app/docs/adding-custom-styles#default-values

This PR documents the `--default(…)` option that can be used in
`--value(…)` and `--modifier(…)` when defining custom functional
utilities in CSS using `@utility`.

This was been implemented in
tailwindlabs/tailwindcss#19989
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[v4] Utility modifiers only work with arguments

1 participant